本章介绍 Rust 与其他编程语言相比最特殊的一个概念: 所有权(ownership),它使得 Rust 在不需要垃圾回收机制的情况保持内存安全。之后我们会介绍与所有权相关的几个概念: borrowing、slice 以及 Rust 的内存布局。
所有权
所有权是 Rust 用于如何管理内存的一组规则。一些语言中例如 Java、C# 具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存。在另一些语言例如 C、C++中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式: 通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查,如果违反了任何这些规则,程序无法编译。
所有权规则
- Rust 中的每一个值都有一个所有者(owner)
- 值在任一时刻有且只有一个所有者
- 当所有者(变量)离开作用域,这个值将被丢弃
变量作用域
作用域是一个项(item)在程序中有效的范围。例如:
|
目前为止,变量是否有效与作用域的关系跟其他编程语言是类似的。
String 类型
除了字符串字面值,Rust 还有另一种字符串类型 String。这个类型管理被分配到堆上的数据,所以能够存储在编译时未知大小的文本。可以使用 from 函数基于字符串字面值来创建 String:
|
我们可以修改此字符串:
|
对于 String 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:
- 必须在运行时向内存分配器 memory allocator 请求内存
- 需要一个当我们处理完 String 时将内存返回给分配器的方法
当调用 String::from 时,它的实现了请求其所需的内存。这在其他编程语言中也是非常通用的。但是将内存返回给分配器不同语言的实现有很大区别,在有垃圾回收的语言中,GC 记录并清除不再使用的内存,而我们并不需要关心它。在没有 GC 的语言中,我们需要自己识别出不再使用的内存并调用代码显式释放。
Rust 采取了一个不同的策略: 内存在拥有它的变量离开作用域后就被自动释放。{
let s = String::from("hello"); // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,
// s 不再有效
当变量离开作用域,Rust 为我们调用一个特殊的 drop 函数。在这里 String 的作者可以放置释放内存的代码。Rust 在作用域结尾处自动调用 drop 函数。
这个模式对编写 Rust 代码的方式有着深远的影响。现在它看起来很简单,不过在更复杂的场景下代码的行为可能是不可预测的。
变量与数据交互的方式: 移动
|
类似其他语言的浅拷贝,上述代码在内存中表现为:
与其他语言不同,之前我们提到过当变量离开作用域后,Rust 自动调用 drop 函数并清理变量的堆内存,这就有了一个问题:当 s2 和 s1 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误。
为了确保内存安全,在 let s2 = s1; 之后,Rust 认为 s1 不再有效,内存中实际表现为:
|
与其他语言的浅拷贝不同,Rust 同时使第一个变量无效,这在 Rust 中称为移动(Move)。
|
注意,当你为现有变量分配一个全新的值时,Rust 将调用 drop 并立即释放原始值的内存。
变量与数据交互的方式: 克隆
也是类似其他编程语言,如果我们确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的通用函数。
|
需要注意: Rust 有一个特殊的注释,称为 Copy 特征,类似整数类型,如果某个类型实现了 Copy 特征,则使用它的变量不会移动,而是会被简单地复制,从而使它们在分配给另一个变量后仍然有效。
|
Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。
哪些类型实现了 Copy trait 的通用规则是:
- 任何一组简单标量值的组合都可以实现 Copy
- 任何不需要分配内存或某种形式资源的类型都可以实现 Copy
所有权与函数参数
将值传递给函数与给变量赋值的原理相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
|
根据上述原则,当尝试在调用 takes_ownership 后使用 s 时,Rust 会抛出一个编译时错误。
返回值与作用域
返回值也可以转移所有权。
|
变量的所有权总是遵循相同的模式: 将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。